﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace CompilerForC.LexicalAnalysis
{
    /// <summary>
    /// Static class used to preprocess source code before tokenization.
    /// </summary>
    internal static class Preprocessor
    {
        #region Public Methods

        /// <summary>
        /// Preforms preprocessing of the specified source code. Currently, only 
        /// single-line and multi-line comments are removed.
        /// </summary>
        /// <param name="source">Source code to preprocess.</param>
        /// <returns>Preprocessed source.</returns>
        public static string Preprocess(string source)
        {
            // Get source code as character array to allow modifications.
            char [] sourceAsArray = (source + Environment.NewLine).ToCharArray();

            // Remove comments.
            RemoveComments(sourceAsArray);

            return new string(sourceAsArray);
        }

        #endregion Public Methods

        #region Private Methods

        /// <summary>
        /// Removes single-line and multi-line comments from the specified 
        /// character array.
        /// </summary>
        /// <param name="source">Source character array.</param>
        private static void RemoveComments(char [] source)
        {
            // Source array index.
            int sourceIndex = 0;
            // Start index of the active comment.
            int commenStartIndex = 0;

            // Flags that we are inside multi-line comment.
            bool insideMultilineComment = false;
            // Flags that we are inside single-line comment.
            bool insideSinglelineComment = false;            
                        
            // For each source character...
            while (sourceIndex < source.Length)
            {
                // Get current character.
                char currentChar = source[sourceIndex];
                // Get previous character
                char previousChar = sourceIndex != 0 ? source[sourceIndex - 1] : char.MinValue;
                
                // If we are inside single-line comment and current character is 
                // new line, perform comment removal...
                if (insideSinglelineComment && IsNewLine(source, sourceIndex))
                {
                    insideSinglelineComment = false;
                    FillWithSpaces(source, commenStartIndex, sourceIndex - commenStartIndex);
                }
                // If we are inside multi-line comment and current and previous 
                // character closes multi-line comment, perform comment removal...
                else if (insideMultilineComment && previousChar == '*' && currentChar == '/')
                {
                    insideMultilineComment = false;
                    FillWithSpaces(source, commenStartIndex, sourceIndex - commenStartIndex + 1);
                }
                // If we are not inside comment...
                else if (!insideSinglelineComment && !insideMultilineComment)
                {
                    // If correct current/previous character combination, flag 
                    // single-line comment.
                    if (previousChar == '/' && currentChar == '/')
                        insideSinglelineComment = true;
                    // If correct current/previous character combination, flag 
                    // multi-line comment.
                    else if (previousChar == '/' && currentChar == '*')
                        insideMultilineComment = true;

                    // If comment is triggered, mark its start index.
                    if (insideSinglelineComment || insideMultilineComment)
                        commenStartIndex = sourceIndex - 1;
                }

                sourceIndex++;
            }
        }

        /// <summary>
        /// Fills specified array with spaces leaving new lines intact.
        /// </summary>
        /// <param name="source">Source character array.</param>
        /// <param name="index">Index where fill begins.</param>
        /// <param name="count">Number of characters to replace with spaces.</param>
        private static void FillWithSpaces(char [] source, int index, int count)
        {
            for (int sourceIndex = index; sourceIndex < index + count; ++sourceIndex)
            {
                if (!IsNewLine(source, sourceIndex))
                    source[sourceIndex] = ' ';
                else
                    sourceIndex += Environment.NewLine.Length - 1;
            }
        }

        /// <summary>
        /// Tests if specified character array, starting at specified index, is 
        /// new line character combination. Performs bounds checking for source 
        /// array.
        /// </summary>
        /// <param name="source">Source character array.</param>
        /// <param name="index">Index to start testing for new line.</param>
        /// <returns>True if new line, false otherwise.</returns>
        private static bool IsNewLine(char [] source, int index)
        {
            // Get new line string...
            string newLine = Environment.NewLine;

            // Combination is new line if all new line characters are matched 
            // with source character array...
            for (int newLineIndex = 0; newLineIndex < newLine.Length; ++newLineIndex)
            {
                int sourceIndex = index + newLineIndex;

                if (sourceIndex >= source.Length || source[sourceIndex] != newLine[newLineIndex])
                    return false;
            }

            return true;
        }

        #endregion Private Methods
    }
}
